package k8;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;

import javax.imageio.ImageIO;

import k8.util.LinkedList;
import k8.util.PixelStoreState;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;

public class Texture
{

	private static LinkedList<Texture> textures = new LinkedList<Texture>();

	private int texture_id;

	private LinkedList.Node<Texture> listnode;

	/** Destroys all Texture instances. */
	public static void destroyAll()
	{
		// Call destroy() on all Texture instances
		LinkedList.Node<Texture> curr = null;
		while ((curr = textures.next(curr)) != null)
			curr.item.destroy();
	}

	/** Creates a new instance of Texture */
	public Texture()
	{
		texture_id = 0;
		listnode = null;
	}

	/** Creates a new instance of Texture */
	public Texture(String name)
	{
		URL url = this.getClass().getResource(name);
		BufferedImage tex = null;
		try
		{
			tex = ImageIO.read(url);
		} catch (IOException e)
		{
			k8.logger.warning("Unable to find texture " + name);
			return;
		}
		generate(tex, GL11.GL_RGB4);
	}

	/**
	 * Generates the texture.
	 * 
	 * @param image
	 *            A BufferedImage
	 * @param interanlFormat
	 *            Internal format used by OpenGL
	 */
	public void generate(BufferedImage image, int internalFormat)
	{
		// Don't proceed if we were given crap data
		if (image == null || internalFormat == 0)
		{
			return;
		}

		// Acquire dimensions and pixel data of image
		int width = image.getWidth();
		int height = image.getHeight();
		byte data[] = (byte[]) image.getRaster().getDataElements(0, 0, width,
				height, null);

		// Put the pixel data of the BufferedImage into a ByteBuffer
		ByteBuffer pixel_buffer = ByteBuffer.allocateDirect(3 * width * height);
		pixel_buffer.put(data);
		pixel_buffer.rewind();

		// Generate the texture
		generate(pixel_buffer, width, height, internalFormat);
	}

	/**
	 * Generates the texture.
	 * 
	 * @param pixel_buffer
	 *            Pixel data
	 * @param width
	 *            Width
	 * @param height
	 *            Height
	 * @param interanlFormat
	 *            Internal format used by OpenGL
	 */
	public void generate(ByteBuffer pixel_buffer, int width, int height,
			int internalFormat)
	{
		// Don't proceed if we were given crap data
		if (pixel_buffer == null || width <= 0 || height <= 0
				|| internalFormat == 0)
		{
			return;
		}

		// Destroy any previous generation
		if (texture_id != 0)
		{
			destroy();
		}

		// Make sure the ByteBuffer is rewound
		pixel_buffer.rewind();

		// Create a texture id
		IntBuffer tex_buffer = ByteBuffer.allocateDirect(4).order(
				ByteOrder.nativeOrder()).asIntBuffer();
		GL11.glGenTextures(tex_buffer);
		texture_id = tex_buffer.get(0);

		// Create a MipMapped texture
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture_id);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S,
				GL11.GL_REPEAT);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T,
				GL11.GL_REPEAT);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER,
				GL11.GL_LINEAR);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER,
				GL11.GL_LINEAR_MIPMAP_LINEAR);
		Texture.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D, internalFormat, width,
				height, GL11.GL_RGB, GL11.GL_UNSIGNED_BYTE, pixel_buffer);

		// Add this texture to the list of generated textures
		listnode = textures.append(this);
	}

	/**
	 * Builds Mipmaps.
	 * 
	 * @param target
	 * @param components
	 * @param width
	 * @param height
	 * @param format
	 * @param type
	 * @param data
	 * 
	 * @return int
	 */
	private static void gluBuild2DMipmaps(final int target,
			final int components, final int width, final int height,
			final int format, final int type, final ByteBuffer data)
	{
		if (width < 1 || height < 1)
			return;

		final int bpp = Texture.bytesPerPixel(format, type);

		if (bpp == 0)
			return;

		final int maxSize = PixelStoreState
				.glGetIntegerv(GL11.GL_MAX_TEXTURE_SIZE);

		int w = Texture.nearestPower(width);
		if (w > maxSize)
			w = maxSize;

		int h = Texture.nearestPower(height);
		if (h > maxSize)
			h = maxSize;

		// Get current glPixelStore state
		PixelStoreState pss = new PixelStoreState();

		// set pixel packing
		GL11.glPixelStorei(GL11.GL_PACK_ROW_LENGTH, 0);
		GL11.glPixelStorei(GL11.GL_PACK_ALIGNMENT, 1);
		GL11.glPixelStorei(GL11.GL_PACK_SKIP_ROWS, 0);
		GL11.glPixelStorei(GL11.GL_PACK_SKIP_PIXELS, 0);

		ByteBuffer image;
		boolean done = false;

		if (w != width || h != height)
		{
			// Must rescale image to get "top" mipmap texture image
			image = BufferUtils.createByteBuffer((w + 4) * h * bpp);
			
			if (!Texture.gluScaleImage(format, width, height, type, data, w, h,
					type, image))
				done = true;

			/* set pixel unpacking */
			GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0);
			GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
			GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0);
			GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0);
		} else
		{
			image = data;
		}

		ByteBuffer bufferA = null;
		ByteBuffer bufferB = null;

		int level = 0;
		while (!done)
		{
			if (image != data)
			{
				/* set pixel unpacking */
				GL11.glPixelStorei(GL11.GL_UNPACK_ROW_LENGTH, 0);
				GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);
				GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_ROWS, 0);
				GL11.glPixelStorei(GL11.GL_UNPACK_SKIP_PIXELS, 0);
			}

			GL11.glTexImage2D(target, level, components, w, h, 0, format, type,
					image);

			if (w == 1 && h == 1)
				break;

			final int newW = (w < 2) ? 1 : w >> 1;
			final int newH = (h < 2) ? 1 : h >> 1;

			final ByteBuffer newImage;

			if (bufferA == null)
				newImage = (bufferA = BufferUtils.createByteBuffer((newW + 4)
						* newH * bpp));
			else if (bufferB == null)
				newImage = (bufferB = BufferUtils.createByteBuffer((newW + 4)
						* newH * bpp));
			else
				newImage = bufferB;

			if (!Texture.gluScaleImage(format, w, h, type, image, newW, newH,
					type, newImage))
				done = true;

			image = newImage;
			if (bufferB != null)
				bufferB = bufferA;

			w = newW;
			h = newH;
			level++;
		}

		// Restore original glPixelStore state
		pss.save();
	}

	/**
	 * Method bytesPerPixel.
	 * 
	 * @param format
	 * @param type
	 * 
	 * @return int
	 */
	private static int bytesPerPixel(int format, int type)
	{
		int n, m;

		switch (format)
		{
		case GL11.GL_COLOR_INDEX:
		case GL11.GL_STENCIL_INDEX:
		case GL11.GL_DEPTH_COMPONENT:
		case GL11.GL_RED:
		case GL11.GL_GREEN:
		case GL11.GL_BLUE:
		case GL11.GL_ALPHA:
		case GL11.GL_LUMINANCE:
			n = 1;
			break;
		case GL11.GL_LUMINANCE_ALPHA:
			n = 2;
			break;
		case GL11.GL_RGB:
		case GL12.GL_BGR:
			n = 3;
			break;
		case GL11.GL_RGBA:
		case GL12.GL_BGRA:
			n = 4;
			break;
		default:
			n = 0;
		}

		switch (type)
		{
		case GL11.GL_UNSIGNED_BYTE:
			m = 1;
			break;
		case GL11.GL_BYTE:
			m = 1;
			break;
		case GL11.GL_BITMAP:
			m = 1;
			break;
		case GL11.GL_UNSIGNED_SHORT:
			m = 2;
			break;
		case GL11.GL_SHORT:
			m = 2;
			break;
		case GL11.GL_UNSIGNED_INT:
			m = 4;
			break;
		case GL11.GL_INT:
			m = 4;
			break;
		case GL11.GL_FLOAT:
			m = 4;
			break;
		default:
			m = 0;
		}

		return n * m;
	}

	/**
	 * Method nearestPower.
	 * 
	 * Compute the nearest power of 2 number. This algorithm is a little
	 * strange, but it works quite well.
	 * 
	 * @param value
	 * 
	 * @return int
	 */
	private static int nearestPower(int value)
	{
		int i = 1;

		/* Error! */
		if (value == 0)
			return -1;

		for (;;)
		{
			if (value == 1)
			{
				return i;
			} else if (value == 3)
			{
				return i << 2;
			}
			value >>= 1;
			i <<= 1;
		}
	}

	/**
	 * Method gluScaleImage.
	 * 
	 * @param format
	 * @param widthIn
	 * @param heightIn
	 * @param typein
	 * @param dataIn
	 * @param widthOut
	 * @param heightOut
	 * @param typeOut
	 * @param dataOut
	 * 
	 * @return int
	 */
	private static boolean gluScaleImage(int format, int widthIn, int heightIn,
			int typein, ByteBuffer dataIn, int widthOut, int heightOut,
			int typeOut, ByteBuffer dataOut)
	{

		final int components = Texture.compPerPix(format);
		if (components == -1)
			return false;

		int i, j, k;
		float[] tempIn, tempOut;
		float sx, sy;
		int sizein, sizeout;
		int rowstride, rowlen;

		// temp image data
		tempIn = new float[widthIn * heightIn * components];
		tempOut = new float[widthOut * heightOut * components];

		// Determine bytes per input type
		switch (typein)
		{
		case GL11.GL_UNSIGNED_BYTE:
			sizein = 1;
			break;
		case GL11.GL_FLOAT:
			sizein = 4;
			break;
		default:
			return false;
		}

		// Determine bytes per output type
		switch (typeOut)
		{
		case GL11.GL_UNSIGNED_BYTE:
			sizeout = 1;
			break;
		case GL11.GL_FLOAT:
			sizeout = 4;
			break;
		default:
			return false;
		}

		// Get glPixelStore state
		PixelStoreState pss = new PixelStoreState();

		// Unpack the pixel data and convert to floating point
		if (pss.unpackRowLength > 0)
			rowlen = pss.unpackRowLength;
		else
			rowlen = widthIn;

		if (sizein >= pss.unpackAlignment)
			rowstride = components * rowlen;
		else
			rowstride = pss.unpackAlignment
					/ sizein
					* Texture.ceil(components * rowlen * sizein,
							pss.unpackAlignment);

		switch (typein)
		{
		case GL11.GL_UNSIGNED_BYTE:
			k = 0;
			dataIn.rewind();
			for (i = 0; i < heightIn; i++)
			{
				int ubptr = i * rowstride + pss.unpackSkipRows * rowstride
						+ pss.unpackSkipPixels * components;
				for (j = 0; j < widthIn * components; j++)
				{
					tempIn[k++] = dataIn.get(ubptr++) & 0xff;
				}
			}
			break;
		case GL11.GL_FLOAT:
			k = 0;
			dataIn.rewind();
			for (i = 0; i < heightIn; i++)
			{
				int fptr = 4 * (i * rowstride + pss.unpackSkipRows * rowstride + pss.unpackSkipPixels
						* components);
				for (j = 0; j < widthIn * components; j++)
				{
					tempIn[k++] = dataIn.getFloat(fptr);
					fptr += 4;
				}
			}
			break;
		default:
			return false;
		}

		// Do scaling
		sx = (float) widthIn / (float) widthOut;
		sy = (float) heightIn / (float) heightOut;

		float[] c = new float[components];
		int src, dst;

		for (int iy = 0; iy < heightOut; iy++)
		{
			for (int ix = 0; ix < widthOut; ix++)
			{
				int x0 = (int) (ix * sx);
				int x1 = (int) ((ix + 1) * sx);
				int y0 = (int) (iy * sy);
				int y1 = (int) ((iy + 1) * sy);

				int readPix = 0;

				// reset weighted pixel
				for (int ic = 0; ic < components; ic++)
				{
					c[ic] = 0;
				}

				// create weighted pixel
				for (int ix0 = x0; ix0 < x1; ix0++)
				{
					for (int iy0 = y0; iy0 < y1; iy0++)
					{

						src = (iy0 * widthIn + ix0) * components;

						for (int ic = 0; ic < components; ic++)
						{
							c[ic] += tempIn[src + ic];
						}

						readPix++;
					}
				}

				// store weighted pixel
				dst = (iy * widthOut + ix) * components;

				if (readPix == 0)
				{
					// Image is sized up, caused by non power of two texture as
					// input
					src = (y0 * widthIn + x0) * components;
					for (int ic = 0; ic < components; ic++)
					{
						tempOut[dst++] = tempIn[src + ic];
					}
				} else
				{
					// sized down
					for (k = 0; k < components; k++)
					{
						tempOut[dst++] = c[k] / readPix;
					}
				}
			}
		}

		// Convert temp output
		if (pss.packRowLength > 0)
			rowlen = pss.packRowLength;
		else
			rowlen = widthOut;

		if (sizeout >= pss.packAlignment)
			rowstride = components * rowlen;
		else
			rowstride = pss.packAlignment
					/ sizeout
					* Texture.ceil(components * rowlen * sizeout,
							pss.packAlignment);

		switch (typeOut)
		{
		case GL11.GL_UNSIGNED_BYTE:
			k = 0;
			for (i = 0; i < heightOut; i++)
			{
				int ubptr = i * rowstride + pss.packSkipRows * rowstride
						+ pss.packSkipPixels * components;

				for (j = 0; j < widthOut * components; j++)
				{
					dataOut.put(ubptr++, (byte) tempOut[k++]);
				}
			}
			break;
		case GL11.GL_FLOAT:
			k = 0;
			for (i = 0; i < heightOut; i++)
			{
				int fptr = 4 * (i * rowstride + pss.unpackSkipRows * rowstride + pss.unpackSkipPixels
						* components);

				for (j = 0; j < widthOut * components; j++)
				{
					dataOut.putFloat(fptr, tempOut[k++]);
					fptr += 4;
				}
			}
			break;
		default:
			return false;
		}

		return true;
	}

	/**
	 * Return ceiling of integer division
	 * 
	 * @param a
	 * @param b
	 * 
	 * @return int
	 */
	private static int ceil(int a, int b)
	{
		return (a % b == 0 ? a / b : a / b + 1);
	}

	/**
	 * Determine number of components per pixel.
	 * 
	 * @param format
	 * 
	 * @return int
	 */
	private static int compPerPix(int format)
	{
		switch (format)
		{
		case GL11.GL_COLOR_INDEX:
		case GL11.GL_STENCIL_INDEX:
		case GL11.GL_DEPTH_COMPONENT:
		case GL11.GL_RED:
		case GL11.GL_GREEN:
		case GL11.GL_BLUE:
		case GL11.GL_ALPHA:
		case GL11.GL_LUMINANCE:
			return 1;
		case GL11.GL_LUMINANCE_ALPHA:
			return 2;
		case GL11.GL_RGB:
		case GL12.GL_BGR:
			return 3;
		case GL11.GL_RGBA:
		case GL12.GL_BGRA:
			return 4;
		default:
			return -1;
		}
	}

	/** Deletes the texture from OpenGL */
	public void destroy()
	{
		if (texture_id != 0)
		{
			IntBuffer tex_buf = ByteBuffer.allocateDirect(4).order(
					ByteOrder.nativeOrder()).asIntBuffer();
			tex_buf.put(texture_id);
			tex_buf.rewind();
			GL11.glDeleteTextures(tex_buf);
			texture_id = 0;
			textures.remove(listnode);
		}
	}

	/** Makes current texture for rendering */
	public void glBindTexture()
	{
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture_id);
	}

}
